<?php
/*
# $Id: View.php 12 2006-11-21 12:51:48Z James $
#
# @event onrender
# Invoked after the View has rendered it's template and stored the result in $this->buffer
*/

/*
# @class View
*/
class View extends Object {

	/*
	# @property string $buffer
	# Stores the result of calling "this->render()".
	*/
	public $buffer = '';

	/*
	# @property Controller $controller
	# This View's Controller.
	*/
	private $controller = NULL;

	/*
	# @property View $globalView
	# A singleton instance of the "GlobalView".
	*/
	static private $globalView = NULL;

	/*
	# @property array $headers
	# Contains all headers that will be included in the HTTP response.
	*/
	private $headers = array();

	/*
	# @property array $renderQueue
	# Stores all View objects that will be rendered when this View's "onrender" event is invoked.
	*/
	private $renderQueue = array();

	/*
	# @property array $slots
	# Each slot contains a nested View object, indexed by a unique identifier.
	*/
	private $slots = array();

	/*
	# @property string $source
	# The template source file's path.
	*/
	protected $source = NULL;

	/*
	# @property string $rawSource
	# The template's raw source markup. This will NOT be evaluated as PHP so any
	# markup it contains will be rendered as-written.
	*/
	protected $rawSource = NULL;

	/*
	# @property array $v
	# Holds all template variables.
	*/
	private $v = array();

	/*
	# @method View __construct( [Controller $controller] )
	# $controller	= The Controller object that is associated with this View
	#
	# Constructor.
	*/
	function __construct($controller=NULL) {

		// Store controller
		$this->controller = $controller;

		// Setup listeners for 'onrender' event
		$this->addEventListener('onrender', array($this, 'processRenderQueue'));
	}

	/*
	# @method void __set( string $varName, mixed $varValue )
	# $varName	= Variable name
	# $varValue	= Variable value
	#
	# Stores the given variable in the self::$v array.
	# Enables you to set a template variable using "$template->myVar = TRUE", rather than "$template->v['myVar'] = TRUE"
	# Because all properties in this class are private they will be seen as non-existent to instances and extended classes.
	*/
	function __set($varName, $varValue) {

		// Set
		$this->v[$varName] = $varValue;
	}

	/*
	# @method mixed __get( string $varName )
	# $varName	= Variable to retrieve
	#
	# Returns $this->v[$varName], or sets it to NULL if it doesn't exist.
	*/
	function __get($varName) {

		// Return
		// NOTE: Setting a non-existent variable allows us to use "$template->myVar[] = TRUE"
		//	 (see http://uk2.php.net/manual/en/language.oop5.overloading.php#69370)
		// NOTE: This still doesn't work, seemingly when an array element contains an Object
		// eg.	$view->objectList[7] = "something";	- "something" never gets stored
		if(!isset($this->v[$varName])) {
			$this->v[$varName] = NULL;
		}

		// Explicitly casting an array fixes the "Indirect modification of overloaded property" bug(?) in PHP5.2+.
		// Converting the array to an ArrayObject returns the variable in read/write mode rather than just read-mode.
		if(is_array($this->v[$varName])) {
			$this->v[$varName] = new ArrayObject($this->v[$varName], ArrayObject::ARRAY_AS_PROPS);
			return $this->v[$varName];
		}
		return $this->v[$varName];
	}

	/*
	# @method void setSource( string $templatePath )
	# $templatePath	= Absolute path to the template file
	#
	# Defines this View's source template that will be used at render-time.
	*/
	function setSource($templatePath=NULL) {

		// Store source
		$this->source = $templatePath;
	}

	/*
	# @method void setRawSource( string $src )
	# $src	= Template source
	#
	# Sets the raw template code, meaning you don't need to specify an external
	# template file.
	*/
	function setRawSource($src=NULL) {

		// Store source
		$this->rawSource = $src;
	}

	/*
	# @method void setHeader( string $header, [string $value] )
	# $header	= Name of the header to be set (eg. Content-Type)
	# $values	= Value to give this header (eg. text/html; charset=utf-8)
	#
	# Prepare an HTTP response header.
	*/
	function setHeader($header, $value=NULL) {

		// Store content-type
		$this->headers[$header] = $value;
	}

	/*
	# @method string getHeader( string $header )
	# $header	= Name of header to retrieve
	#
	# Returns the value of the specified header.
	*/
	public function getHeader($header) {
		return isset($this->headers[$header]) ? $this->headers[$header] : '';
	}

	/*
	# @method void attachViewToSlot( View $view, string $slotId )
	# $view		= View object to be attached
	# $slotId	= Unique slot identifier
	#
	# Attaches the View object to the specified slot.
	*/
	function attachViewToSlot($view, $slotId) {

		// Store View
		$this->slots[$slotId] = $view;
	}

	/*
	# @method View getSlot( string $slotId )
	# $slotId	= ID of the slot you want to retrieve
	#
	# Returns the View stored in the specified slot, or an empty View if the slot does not exist.
	*/
	public function getSlot($slotId) {

		// Result
		if(isset($this->slots[$slotId])) {
			return $this->slots[$slotId];
		}
		else {
			SystemLog::add("There is no View object in slot \"{$slotId}\"", SystemLog::WARNING);
			return new View();
		}
	}

	/*
	# @method string render()
	#
	# Renders this View.
	*/
	function render() {

		// Start output buffering
		ob_start();

		// Send headers
		// Thanks to output buffering nested Views can set headers too without
		// worrying about them already having been output.
		foreach($this->headers as $header=>$value) {
			header($header.($value===NULL ? '' : ": {$value}"));
		}
		unset($header, $value);

		// Extract all variables into the local scope
		extract($this->v, EXTR_REFS);

		// Include PHP source template
		if($this->source===NULL) {
			// No external source template has been defined, so check the rawSource
			if($this->rawSource!==NULL) {
				echo $this->rawSource;
			}
		}
		else if(is_file($this->source) && include($this->source)) {
			// Successfully included
		}
		else {
			SystemLog::add("View template source is missing: {$this->source}", SystemLog::WARNING);
		}

		// Read and clean the output buffer
		$buffer = ob_get_clean();

		// Remove any UTF-8 BOM (Byte Order Mark) from the beginning of the buffer
		// This is required, at least, on child templates because otherwise the
		// BOM is displayed as a visible character in the output.
		$buffer = preg_replace("/^\xef\xbb\xbf/", "", $buffer);

		// Store buffer
		$this->buffer = $buffer;

		// Invoke "onrender" event
		$this->invokeEvent('onrender');

		// Result
		return $this->buffer;
	}

	/*
	# @method string renderAfter( View $view )
	# $view	= The View after which $this View will be rendered and then substituted into $view
	#
	# Tells the rendering queue to wait until $view has been rendered before then rendering $this
	# and substituting the result into $view's output buffer.
	# The returned string is a placeholder tag that will be replaced with $this' rendered output.
	*/
	public function renderAfter($view) {

		// Add $this View to the $view's "onrender" queue
		return $view->addToRenderQueue($this);
	}

	/*
	# @method void processRenderQueue()
	#
	# Renders all Views in $this->renderQueue, substituting the buffer tags in the calling View
	# with the results of these renders.
	*/
	public function processRenderQueue() {

		// renders all views in $this->postRenderViews and substitutes their renderTag in $this->buffer
		foreach($this->renderQueue as $bufferTag=>$view) {
			$vbuffer = $view->render();
			$this->buffer = str_replace($bufferTag, $vbuffer, $this->buffer);
		}
	}

	/*
	# @method string addToRenderQueue( View $view )
	# $view	= View instance
	#
	# Appends $view to $this->renderQueue and returns it's allocated buffer-tag.
	*/
	function addToRenderQueue($view) {
		$bufferTag = "[[buffertag:".(md5(rand(0,999999)))."]]";
		$this->renderQueue[$bufferTag] = $view;
		return $bufferTag;
	}

	/*
	# @method View getGlobalView()
	#
	# Returns the singleton instance of the "Global View" object.
	#
	# TODO:
	# It might actually be a good idea to retrieve the global view object afresh each call to this function.
	# Keep it as a static class property though because if it does change then all references to it will also change accordingly.
	*/
	static public function getGlobalView() {

		// Create the singleton instance
		if(self::$globalView===NULL) {
			$gvCommand = UrlCommand::create('/global-view');
			self::$globalView = $gvCommand->execute();
		}

		// Result
		return self::$globalView;
	}

	/*
	# @method void setGlobalView( [View $view] )
	# $view	= View to be used as the GlobalView
	#
	# Allows you to replace the GlobalView with a custom View instance.
	*/
	static public function setGlobalView($view=NULL) {

		// Store
		self::$globalView = $view;
	}
}
?>